import { isArray, formatNum } from '../core/Util';

/*
 * @class Point
 * @aka L.Point
 *
 * Represents a point with `x` and `y` coordinates in pixels.
 *
 * @example
 *
 * ```js
 * var point = L.point(200, 300);
 * ```
 *
 * All Leaflet methods and options that accept `Point` objects also accept them in a simple Array form (unless noted otherwise), so these lines are equivalent:
 *
 * ```js
 * map.panBy([200, 300]);
 * map.panBy(L.point(200, 300));
 * ```
 *
 * Note that `Point` does not inherit from Leaflet's `Class` object,
 * which means new classes can't inherit from it, and new methods
 * can't be added to it with the `include` function.
 */

export function Point(x, y, round) {
  // @property x: Number; The `x` coordinate of the point
  this.x = round ? Math.round(x) : x;
  // @property y: Number; The `y` coordinate of the point
  this.y = round ? Math.round(y) : y;
}

var trunc =
  Math.trunc ||
  function (v) {
    return v > 0 ? Math.floor(v) : Math.ceil(v);
  };

Point.prototype = {
  // @method clone(): Point
  // Returns a copy of the current point.
  clone: function () {
    return new Point(this.x, this.y);
  },

  // @method add(otherPoint: Point): Point
  // Returns the result of addition of the current and the given points.
  add: function (point) {
    // non-destructive, returns a new point
    return this.clone()._add(toPoint(point));
  },

  _add: function (point) {
    // destructive, used directly for performance in situations where it's safe to modify existing point
    this.x += point.x;
    this.y += point.y;
    return this;
  },

  // @method subtract(otherPoint: Point): Point
  // Returns the result of subtraction of the given point from the current.
  subtract: function (point) {
    return this.clone()._subtract(toPoint(point));
  },

  _subtract: function (point) {
    this.x -= point.x;
    this.y -= point.y;
    return this;
  },

  // @method divideBy(num: Number): Point
  // Returns the result of division of the current point by the given number.
  divideBy: function (num) {
    return this.clone()._divideBy(num);
  },

  _divideBy: function (num) {
    this.x /= num;
    this.y /= num;
    return this;
  },

  // @method multiplyBy(num: Number): Point
  // Returns the result of multiplication of the current point by the given number.
  multiplyBy: function (num) {
    return this.clone()._multiplyBy(num);
  },

  _multiplyBy: function (num) {
    this.x *= num;
    this.y *= num;
    return this;
  },

  // @method scaleBy(scale: Point): Point
  // Multiply each coordinate of the current point by each coordinate of
  // `scale`. In linear algebra terms, multiply the point by the
  // [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation)
  // defined by `scale`.
  scaleBy: function (point) {
    return new Point(this.x * point.x, this.y * point.y);
  },

  // @method unscaleBy(scale: Point): Point
  // Inverse of `scaleBy`. Divide each coordinate of the current point by
  // each coordinate of `scale`.
  unscaleBy: function (point) {
    return new Point(this.x / point.x, this.y / point.y);
  },

  // @method round(): Point
  // Returns a copy of the current point with rounded coordinates.
  round: function () {
    return this.clone()._round();
  },

  _round: function () {
    this.x = Math.round(this.x);
    this.y = Math.round(this.y);
    return this;
  },

  // @method floor(): Point
  // Returns a copy of the current point with floored coordinates (rounded down).
  floor: function () {
    return this.clone()._floor();
  },

  _floor: function () {
    this.x = Math.floor(this.x);
    this.y = Math.floor(this.y);
    return this;
  },

  // @method ceil(): Point
  // Returns a copy of the current point with ceiled coordinates (rounded up).
  ceil: function () {
    return this.clone()._ceil();
  },

  _ceil: function () {
    this.x = Math.ceil(this.x);
    this.y = Math.ceil(this.y);
    return this;
  },

  // @method trunc(): Point
  // Returns a copy of the current point with truncated coordinates (rounded towards zero).
  trunc: function () {
    return this.clone()._trunc();
  },

  _trunc: function () {
    this.x = trunc(this.x);
    this.y = trunc(this.y);
    return this;
  },

  // @method distanceTo(otherPoint: Point): Number
  // Returns the cartesian distance between the current and the given points.
  distanceTo: function (point) {
    point = toPoint(point);

    var x = point.x - this.x,
      y = point.y - this.y;

    return Math.sqrt(x * x + y * y);
  },

  // @method equals(otherPoint: Point): Boolean
  // Returns `true` if the given point has the same coordinates.
  equals: function (point) {
    point = toPoint(point);

    return point.x === this.x && point.y === this.y;
  },

  // @method contains(otherPoint: Point): Boolean
  // Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values).
  contains: function (point) {
    point = toPoint(point);

    return (
      Math.abs(point.x) <= Math.abs(this.x) &&
      Math.abs(point.y) <= Math.abs(this.y)
    );
  },

  // @method toString(): String
  // Returns a string representation of the point for debugging purposes.
  toString: function () {
    return 'Point(' + formatNum(this.x) + ', ' + formatNum(this.y) + ')';
  },
};

// @factory L.point(x: Number, y: Number, round?: Boolean)
// Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values.

// @alternative
// @factory L.point(coords: Number[])
// Expects an array of the form `[x, y]` instead.

// @alternative
// @factory L.point(coords: Object)
// Expects a plain object of the form `{x: Number, y: Number}` instead.
export function toPoint(x, y, round) {
  if (x instanceof Point) {
    return x;
  }
  if (isArray(x)) {
    return new Point(x[0], x[1]);
  }
  if (x === undefined || x === null) {
    return x;
  }
  if (typeof x === 'object' && 'x' in x && 'y' in x) {
    return new Point(x.x, x.y);
  }
  return new Point(x, y, round);
}
